Passed
Branch wavefile-reader (73452d)
by Rafael S.
06:21
created

WaveFile.listCuePoints   A

Complexity

Conditions 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
dl 0
loc 9
rs 10
c 0
b 0
f 0
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFile class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import {encode, decode} from 'base64-arraybuffer-es6';
33
import WaveFileConverter from './lib/wavefile-converter';
34
import fixRIFFTag from './lib/fix-riff-tag';
35
36
/**
37
 * A class to manipulate wav files.
38
 * @extends WaveFileConverter
39
 */
40
export default class WaveFile extends WaveFileConverter {
41
42
  /**
43
   * Use a .wav file encoded as a base64 string to load the WaveFile object.
44
   * @param {string} base64String A .wav file as a base64 string.
45
   * @throws {Error} If any property of the object appears invalid.
46
   */
47
  fromBase64(base64String) {
48
    this.fromBuffer(new Uint8Array(decode(base64String)));
49
  }
50
51
  /**
52
   * Return a base64 string representig the WaveFile object as a .wav file.
53
   * @return {string} A .wav file as a base64 string.
54
   * @throws {Error} If any property of the object appears invalid.
55
   */
56
  toBase64() {
57
    /** @type {!Uint8Array} */
58
    let buffer = this.toBuffer();
59
    return encode(buffer, 0, buffer.length);
60
  }
61
62
  /**
63
   * Return a DataURI string representig the WaveFile object as a .wav file.
64
   * The return of this method can be used to load the audio in browsers.
65
   * @return {string} A .wav file as a DataURI.
66
   * @throws {Error} If any property of the object appears invalid.
67
   */
68
  toDataURI() {
69
    return 'data:audio/wav;base64,' + this.toBase64();
70
  }
71
72
  /**
73
   * Use a .wav file encoded as a DataURI to load the WaveFile object.
74
   * @param {string} dataURI A .wav file as DataURI.
75
   * @throws {Error} If any property of the object appears invalid.
76
   */
77
  fromDataURI(dataURI) {
78
    this.fromBase64(dataURI.replace('data:audio/wav;base64,', ''));
79
  }
80
81
  /**
82
   * Write a RIFF tag in the INFO chunk. If the tag do not exist,
83
   * then it is created. It if exists, it is overwritten.
84
   * @param {string} tag The tag name.
85
   * @param {string} value The tag value.
86
   * @throws {Error} If the tag name is not valid.
87
   */
88
  setTag(tag, value) {
89
    tag = fixRIFFTag(tag);
90
    /** @type {!Object} */
91
    let index = this.getTagIndex_(tag);
92
    if (index.TAG !== null) {
93
      this.LIST[index.LIST].subChunks[index.TAG].chunkSize =
94
        value.length + 1;
95
      this.LIST[index.LIST].subChunks[index.TAG].value = value;
96
    } else if (index.LIST !== null) {
97
      this.LIST[index.LIST].subChunks.push({
98
        chunkId: tag,
99
        chunkSize: value.length + 1,
100
        value: value});
101
    } else {
102
      this.LIST.push({
103
        chunkId: 'LIST',
104
        chunkSize: 8 + value.length + 1,
105
        format: 'INFO',
106
        subChunks: []});
107
      this.LIST[this.LIST.length - 1].subChunks.push({
108
        chunkId: tag,
109
        chunkSize: value.length + 1,
110
        value: value});
111
    }
112
  }
113
114
  /**
115
   * Remove a RIFF tag from the INFO chunk.
116
   * @param {string} tag The tag name.
117
   * @return {boolean} True if a tag was deleted.
118
   */
119
  deleteTag(tag) {
120
    /** @type {!Object} */
121
    let index = this.getTagIndex_(tag);
122
    if (index.TAG !== null) {
123
      this.LIST[index.LIST].subChunks.splice(index.TAG, 1);
124
      return true;
125
    }
126
    return false;
127
  }
128
129
  /**
130
   * Create a cue point in the wave file.
131
   * @param {number} position The cue point position in milliseconds.
132
   * @param {string} labl The LIST adtl labl text of the marker. Optional.
133
   */
134
  setCuePoint(position, labl='') {
135
    this.cue.chunkId = 'cue ';
136
    position = (position * this.fmt.sampleRate) / 1000;
137
    /** @type {!Array<!Object>} */
138
    let existingPoints = this.getCuePoints_();
139
    this.clearLISTadtl_();
140
    /** @type {number} */
141
    let len = this.cue.points.length;
142
    this.cue.points = [];
143
    /** @type {boolean} */
144
    let hasSet = false;
145
    if (len === 0) {
146
      this.setCuePoint_(position, 1, labl);
147
    } else {
148
      for (let i = 0; i < len; i++) {
149
        if (existingPoints[i].dwPosition > position && !hasSet) {
150
          this.setCuePoint_(position, i + 1, labl);
151
          this.setCuePoint_(
152
            existingPoints[i].dwPosition,
153
            i + 2,
154
            existingPoints[i].label);
155
          hasSet = true;
156
        } else {
157
          this.setCuePoint_(
158
            existingPoints[i].dwPosition,
159
            i + 1,
160
            existingPoints[i].label);
161
        }
162
      }
163
      if (!hasSet) {
164
        this.setCuePoint_(position, this.cue.points.length + 1, labl);
165
      }
166
    }
167
    this.cue.dwCuePoints = this.cue.points.length;
168
  }
169
170
  /**
171
   * Remove a cue point from a wave file.
172
   * @param {number} index the index of the point. First is 1,
173
   *    second is 2, and so on.
174
   */
175
  deleteCuePoint(index) {
176
    this.cue.chunkId = 'cue ';
177
    /** @type {!Array<!Object>} */
178
    let existingPoints = this.getCuePoints_();
179
    this.clearLISTadtl_();
180
    /** @type {number} */
181
    let len = this.cue.points.length;
182
    this.cue.points = [];
183
    for (let i = 0; i < len; i++) {
184
      if (i + 1 !== index) {
185
        this.setCuePoint_(
186
          existingPoints[i].dwPosition,
187
          i + 1,
188
          existingPoints[i].label);
189
      }
190
    }
191
    this.cue.dwCuePoints = this.cue.points.length;
192
    if (this.cue.dwCuePoints) {
193
      this.cue.chunkId = 'cue ';
194
    } else {
195
      this.cue.chunkId = '';
196
      this.clearLISTadtl_();
197
    }
198
  }
199
200
  /**
201
   * Update the label of a cue point.
202
   * @param {number} pointIndex The ID of the cue point.
203
   * @param {string} label The new text for the label.
204
   */
205
  updateLabel(pointIndex, label) {
206
    /** @type {?number} */
207
    let cIndex = this.getAdtlChunk_();
208
    if (cIndex !== null) {
209
      for (let i = 0, len = this.LIST[cIndex].subChunks.length; i < len; i++) {
210
        if (this.LIST[cIndex].subChunks[i].dwName ==
211
            pointIndex) {
212
          this.LIST[cIndex].subChunks[i].value = label;
213
        }
214
      }
215
    }
216
  }
217
218
  /**
219
   * Push a new cue point in this.cue.points.
220
   * @param {number} position The position in milliseconds.
221
   * @param {number} dwName the dwName of the cue point
222
   * @private
223
   */
224
  setCuePoint_(position, dwName, label) {
225
    this.cue.points.push({
226
      dwName: dwName,
227
      dwPosition: position,
228
      fccChunk: 'data',
229
      dwChunkStart: 0,
230
      dwBlockStart: 0,
231
      dwSampleOffset: position,
232
    });
233
    this.setLabl_(dwName, label);
234
  }
235
236
  /**
237
   * Clear any LIST chunk labeled as 'adtl'.
238
   * @private
239
   */
240
  clearLISTadtl_() {
241
    for (let i = 0, len = this.LIST.length; i < len; i++) {
242
      if (this.LIST[i].format == 'adtl') {
243
        this.LIST.splice(i);
244
      }
245
    }
246
  }
247
248
  /**
249
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
250
   * @param {number} dwName The ID of the cue point.
251
   * @param {string} label The label for the cue point.
252
   * @private
253
   */
254
  setLabl_(dwName, label) {
255
    /** @type {?number} */
256
    let adtlIndex = this.getAdtlChunk_();
257
    if (adtlIndex === null) {
258
      this.LIST.push({
259
        chunkId: 'LIST',
260
        chunkSize: 4,
261
        format: 'adtl',
262
        subChunks: []});
263
      adtlIndex = this.LIST.length - 1;
264
    }
265
    this.setLabelText_(adtlIndex === null ? 0 : adtlIndex, dwName, label);
266
  }
267
268
  /**
269
   * Create a new 'labl' subchunk in a 'LIST' chunk of type 'adtl'.
270
   * @param {number} adtlIndex The index of the 'adtl' LIST in this.LIST.
271
   * @param {number} dwName The ID of the cue point.
272
   * @param {string} label The label for the cue point.
273
   * @private
274
   */
275
  setLabelText_(adtlIndex, dwName, label) {
276
    this.LIST[adtlIndex].subChunks.push({
277
      chunkId: 'labl',
278
      chunkSize: label.length,
279
      dwName: dwName,
280
      value: label
281
    });
282
    this.LIST[adtlIndex].chunkSize += label.length + 4 + 4 + 4 + 1;
283
  }
284
}
285